package top.v5it.japi.plus.log;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * 默认实现，将日志写入文件中
 *
 * @author zhanpu
 * @date 2021/3/25
 */
@Slf4j
public class FileWriterOperLogServiceImpl implements OperLogService {

    /**
     * 日志文件名前缀
     */
    private static final String PREFIX_FILENAME = "oper_";

    /**
     * 日志文件名后缀
     */
    private static final String POSTFIX_FILENAME = ".log";

    /**
     * 单个日志文件记录最大数目
     */
    private static final long MAX_LINE = 50000L;

    /**
     * 默认日志存储路径
     */
    private static final String DEFAULT_LOG_DIR = "/var/japi/logs";

    /**
     * 设置log日志存储目录的系统环境参数名，按JVM参数格式设置-Doperlog.dir=/var/jsapi/logs
     */
    private static final String LOG_DIR_KEY = "operlog.dir";

    /**
     * 获取日志存储路径
     */
    private static final Supplier<String> SUPPLIER_LOG_DIR = () -> System.getProperty(LOG_DIR_KEY, DEFAULT_LOG_DIR) + File.separator;

    /**
     * 获取当天日期
     */
    private static final Supplier<String> SUPPLIER_LOCAL_DATE = () -> LocalDate.now(ZoneId.systemDefault()).toString();

    /**
     * 获取日志文件全名
     */
    private static final Supplier<String> SUPPLIER_FILENAME = () -> PREFIX_FILENAME + SUPPLIER_LOCAL_DATE.get() + POSTFIX_FILENAME;

    /**
     * 获取日志全路径
     */
    private static final Function<String, String> SUPPLIER_FILEPATH = v -> SUPPLIER_LOG_DIR.get() + SUPPLIER_LOCAL_DATE.get() + File.separator + v;

    /**
     * 日志收集对列
     */
    private final BlockingQueue<OperLog> logQueue;

    /**
     * 日志文件名
     */
    private String filename = "";

    /**
     * 记录当天日期
     */
    private String today;

    /**
     * 记录单个日志数量
     */
    private volatile long count = 0L;

    @SuppressWarnings("unchecked")
    public FileWriterOperLogServiceImpl() {
        this.logQueue = new LinkedBlockingQueue();
        this.today = SUPPLIER_LOCAL_DATE.get();
        this.createLogFile();
        this.run();
    }

    /**
     * 将日志暂写入对列中
     *
     * @param operLog 日志记录
     */
    @Override
    public void addOperLog(OperLog operLog) {
        logQueue.add(operLog);
    }

    /**
     * 异步线程写入日志文件
     */
    void run() {

        ThreadUtil.newThread(() -> {

                    while (true) {
                        try {
                            TimeUnit.SECONDS.sleep(5L);
                            this.writer();
                        } catch (Exception e) {
                            log.error(e.getMessage(), e);
                        }

                    }

                }
                , "writer-log-thread", true)
                .start();

        log.info("writer-log-thread startup ...");

        /**
         * 注册jvm关闭后操作处理线程
         */
        RuntimeUtil.addShutdownHook(() -> {
            log.warn("程序关闭中，正在写入日志文件...");
            this.writer();
        });

    }

    /**
     * 写入日志
     */
    private void writer() {
        List<OperLog> operLogList = new ArrayList<>();
        logQueue.drainTo(operLogList);

        this.writer(operLogList);
    }

    /**
     * 写文件
     *
     * @param operLogList 内容
     */
    private void writer(List<OperLog> operLogList) {
        if (CollUtil.isEmpty(operLogList)) {
            return;
        }

        if (isBackup()) {
            this.backup();
        }

        if (isNewDay()) {
            this.reset();
            this.createLogFile();
        }

        long diff = MAX_LINE - count;
        int size = operLogList.size();
        int compare = NumberUtil.compare(diff, size);

        if (compare < 0) {

            List<OperLog> newList = CollUtil.sub(operLogList, 0, Long.valueOf(diff).intValue());
            this.doWriter(newList);

            newList.clear();

            newList = CollUtil.sub(operLogList, Long.valueOf(diff).intValue(), -1);
            this.writer(newList);
        } else {
            this.doWriter(operLogList);
        }

    }

    /**
     * 将内容写入文件
     *
     * @param operLogList 内容
     */
    private void doWriter(List<OperLog> operLogList) {
        String filepath = SUPPLIER_FILEPATH.apply(SUPPLIER_FILENAME.get());
        FileUtil.writeLines(operLogList, filepath, CharsetUtil.CHARSET_UTF_8, true);

        count += operLogList.size();
    }

    /**
     * 创建日志文件
     */
    private void createLogFile() {

        if (!filename.contains(SUPPLIER_LOCAL_DATE.get()) && count == 0L) {

            String filepath = SUPPLIER_FILEPATH.apply(SUPPLIER_FILENAME.get());

            if (!FileUtil.exist(filepath)) {
                File file = FileUtil.newFile(filepath);
                this.filename = file.getName();
                return;
            }
        }

        try {
            count = this.statisticsLine();
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

    /**
     * 重置
     */
    private void reset() {
        this.filename = "";
        this.count = 0L;
    }

    /**
     * 是否备份
     *
     * @return true-备份，false-不备份；
     */
    private boolean isBackup() {
        return MAX_LINE == count;
    }

    /**
     * 是否新的一天
     *
     * @return true-是，false-不是；
     */
    private boolean isNewDay() {
        String today = SUPPLIER_LOCAL_DATE.get();

        if (this.today.equals(today)) {
            return false;
        } else {
            this.today = today;
            return true;
        }
    }

    /**
     * 备份文件
     */
    private void backup() {

        String localDateTimeStr = LocalDateTime
                .now(ZoneId.systemDefault())
                .format(DateTimeFormatter.ofPattern(DatePattern.PURE_DATETIME_MS_PATTERN));

        String filepath = SUPPLIER_FILEPATH.apply(SUPPLIER_FILENAME.get());
        String newName = SUPPLIER_FILEPATH.apply(this.filename.replace(SUPPLIER_LOCAL_DATE.get(), localDateTimeStr));
        FileUtil.rename(FileUtil.file(filepath), newName, true);

        this.reset();
        this.createLogFile();
    }

    /**
     * 调用外部命令统计文件行数
     *
     * @return 日志内容行数
     */
    private long statisticsLine() {
        String[] cmds;
        String filepath = SUPPLIER_FILEPATH.apply(SUPPLIER_FILENAME.get());
        OsInfo osInfo = SystemUtil.getOsInfo();

        if (osInfo.isWindows()) {
            cmds = new String[]{"type " + filepath + " | find /v /c \"\""};
        } else if (osInfo.isAix() || osInfo.isHpUx() || osInfo.isLinux() || osInfo.isMac()) {
            cmds = new String[]{"/bin/sh", "-c", "grep '' " + filepath + " | wc -l"};
        } else {
            return 0;
        }

        String result = RuntimeUtil.execForStr(cmds);

        return StrUtil.isBlank(result) ? 0L : Long.parseLong(StrUtil.trim(result));
    }

}
